package jacky.lanlan.song.concurrent;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

/**
 * 有优先级的Semaphore。
 * <p>
 * 当acquire的数量耗尽后，后来的线程都将排队等待，FIFO，第一个等待的线程在acquire可用时将第一个
 * 申请到。
 */
public class FIFOSemaphore {

	private Semaphore semaphore;
	
	private BlockingQueue<Long> waitQueue = new LinkedBlockingQueue<Long>();
	
	private Map<Long,CountDownLatch> signals = new WeakHashMap<Long,CountDownLatch>();
	
	private Lock regLock = new ReentrantLock();

	private Lock unregLock = new ReentrantLock();
	
	public FIFOSemaphore(int initial) {
		semaphore = new Semaphore(initial);
	}

	/**
	 * 获得一个许可证。
	 * <p>
	 * <i>当没有许可证可用时，该方法将阻塞直到有许可证可用。</i>
	 * @throws InterruptedException 如果当前线程被打断
	 * @see Semaphore#acquire()
	 */
	public void acquire() throws InterruptedException {
		if(semaphore.tryAcquire()) return;
		
		regLock.lock();
		try {
			registerToQueue();// 把自己注册到等待队列
		}
		finally {
			regLock.unlock();
		}
		
		signals.get(Thread.currentThread().getId()).await();// 阻塞，等待通知
		//(这里有致命风险，因为线程的真正启动时间是不可控制的，很有可能后通知的线程却先启动，进而先获得许可证，从而破坏FIFO)
		// 事实上，我只能做到通知的FIFO，而真正让线程动起来却是不可控的
		
		semaphore.acquire();// 接到通知，开始请求许可证。一定可以拿到，见release()实现
	}

	private void registerToQueue() {
		long id = Thread.currentThread().getId();
		waitQueue.add(id);
		if(signals.containsKey(id)) {// 这种情况是因为某个线程在acquire()以后的执行中由于某种原因死了，JVM会回收ID赋予一个新线程
			signals.remove(id);// 必须要删除这个entry，因为已经失效了，占用这个entry的CountDownLatch的线程已经没有了
		}
		signals.put(id, new CountDownLatch(1));
	}

	private void notifyFirstWaiterAndUnreg() {
		long id = waitQueue.poll();
		CountDownLatch latch= signals.get(id);
		signals.remove(id);
		latch.countDown();// 通知它去申请
	}
	
	/**
	 * 释放许可证。
	 * @see Semaphore#release()
	 */
	public void release() {
		semaphore.release();// 某个线程归还许可证

		unregLock.lock();
		try {
			// 想象如果没有锁，并且有n个线程同时走到这里，而队列中却又只有<n个等待ID，waitQueue.poll()会导致空指针
			if(isSomeOneWaiting()) {
				notifyFirstWaiterAndUnreg();// 通知排在第一个的等待线程去拿许可证，然后unregister它
			}
		}
		finally {
			unregLock.unlock();
		}
	}

	/**
	 * 是否有线程在等待。
	 * <p>
	 * <i>注意，返回的是方法调用那一刻的等待情况，结果可能过期。</i>
	 */
	public boolean isSomeOneWaiting() {
		return !waitQueue.isEmpty();
	}

	/**
	 * 得到正在等待的线程ID列表。
	 * <p>
	 * <i>注意，返回的是方法调用那一刻的等待情况，列表可能过期。</i>
	 */
	public List<Long> getWaiterIDList() {
		List<Long> list = new ArrayList<Long>(waitQueue.size());
		for (long id : waitQueue) {
			list.add(id);
		}
		return list;
	}
	
}
